/*
* Copyright 2014 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.realm;
import android.annotation.TargetApi;
import android.app.IntentService;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import android.util.JsonReader;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import io.realm.exceptions.RealmException;
import io.realm.exceptions.RealmFileException;
import io.realm.exceptions.RealmMigrationNeededException;
import io.realm.internal.ColumnIndices;
import io.realm.internal.ColumnInfo;
import io.realm.internal.ObjectServerFacade;
import io.realm.internal.OsObject;
import io.realm.internal.RealmCore;
import io.realm.internal.RealmNotifier;
import io.realm.internal.RealmObjectProxy;
import io.realm.internal.RealmProxyMediator;
import io.realm.internal.SharedRealm;
import io.realm.internal.Table;
import io.realm.internal.async.RealmAsyncTaskImpl;
import io.realm.log.RealmLog;
import rx.Observable;
/**
* The Realm class is the storage and transactional manager of your object persistent store. It is in charge of creating
* instances of your RealmObjects. Objects within a Realm can be queried and read at any time. Creating, modifying, and
* deleting objects must be done while inside a transaction. See {@link #executeTransaction(Transaction)}
* <p>
* The transactions ensure that multiple instances (on multiple threads) can access the same objects in a consistent
* state with full ACID guarantees.
* <p>
* It is important to remember to call the {@link #close()} method when done with a Realm instance. Failing to do so can
* lead to {@link java.lang.OutOfMemoryError} as the native resources cannot be freed.
* <p>
* Realm instances cannot be used across different threads. This means that you have to open an instance on each thread
* you want to use Realm. Realm instances are cached automatically per thread using reference counting, so as long as
* the reference count doesn't reach zero, calling {@link #getInstance(RealmConfiguration)} will just return the cached
* Realm and should be considered a lightweight operation.
* <p>
* For the UI thread this means that opening and closing Realms should occur in either onCreate/onDestroy or
* onStart/onStop.
* <p>
* Realm instances coordinate their state across threads using the {@link android.os.Handler} mechanism. This also means
* that Realm instances on threads without a {@link android.os.Looper} cannot receive updates unless {@link #waitForChange()}
* is manually called.
* <p>
* A standard pattern for working with Realm in Android activities can be seen below:
* <p>
* <pre>
* public class RealmApplication extends Application {
*
* \@Override
* public void onCreate() {
* super.onCreate();
*
* // The Realm file will be located in package's "files" directory.
* RealmConfiguration realmConfig = new RealmConfiguration.Builder(this).build();
* Realm.setDefaultConfiguration(realmConfig);
* }
* }
*
* public class RealmActivity extends Activity {
*
* private Realm realm;
*
* \@Override
* protected void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* setContentView(R.layout.layout_main);
* realm = Realm.getDefaultInstance();
* }
*
* \@Override
* protected void onDestroy() {
* super.onDestroy();
* realm.close();
* }
* }
* </pre>
* <p>
* Realm supports String and byte fields containing up to 16 MB.
* <p>
*
* @see <a href="http://en.wikipedia.org/wiki/ACID">ACID</a>
* @see <a href="https://github.com/realm/realm-java/tree/master/examples">Examples using Realm</a>
*/
public class Realm extends BaseRealm {
private static final String NULL_CONFIG_MSG = "A non-null RealmConfiguration must be provided";
public static final String DEFAULT_REALM_NAME = RealmConfiguration.DEFAULT_REALM_NAME;
private static RealmConfiguration defaultConfiguration;
/**
* The constructor is private to enforce the use of the static one.
*
* @param cache the {@link RealmCache} associated to this Realm instance.
* @throws IllegalArgumentException if trying to open an encrypted Realm with the wrong key.
*/
private Realm(RealmCache cache) {
super(cache);
}
/**
* {@inheritDoc}
*/
@Override
public Observable<Realm> asObservable() {
return configuration.getRxFactory().from(this);
}
/**
* Initializes the Realm library and creates a default configuration that is ready to use. It is required to call
* this method before interacting with any other of the Realm API's.
* <p>
* A good place is in an {@link android.app.Application} subclass:
* <pre>
* {@code
* public class MyApplication extends Application {
* \@Override
* public void onCreate() {
* super.onCreate();
* Realm.init(this);
* }
* }
* }
* </pre>
* <p>
* Remember to register it in the {@code AndroidManifest.xml} file:
* <pre>
* {@code
* <?xml version="1.0" encoding="utf-8"?>
* <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.realm.example">
* <application android:name=".MyApplication">
* // ...
* </application>
* </manifest>
* }
* </pre>
*
* @param context the Application Context.
* @throws IllegalArgumentException if a {@code null} context is provided.
* @throws IllegalStateException if {@link Context#getFilesDir()} could not be found.
* @see #getDefaultInstance()
*/
public static synchronized void init(Context context) {
if (BaseRealm.applicationContext == null) {
if (context == null) {
throw new IllegalArgumentException("Non-null context required.");
}
checkFilesDirAvailable(context);
RealmCore.loadLibrary(context);
defaultConfiguration = new RealmConfiguration.Builder(context).build();
ObjectServerFacade.getSyncFacadeIfPossible().init(context);
BaseRealm.applicationContext = context.getApplicationContext();
SharedRealm.initialize(new File(context.getFilesDir(), ".realm.temp"));
}
}
/**
* In some cases, Context.getFilesDir() is not available when the app launches the first time.
* This should never happen according to the official Android documentation, but the race condition wasn't fixed
* until Android 4.4.
* <p>
* This method attempts to fix that situation. If this doesn't work an {@link IllegalStateException} will be
* thrown.
* <p>
* See these links for further details:
* https://issuetracker.google.com/issues/36918154
* https://github.com/realm/realm-java/issues/4493#issuecomment-295349044
*/
private static void checkFilesDirAvailable(Context context) {
File filesDir = context.getFilesDir();
if (filesDir != null) {
if (filesDir.exists()) {
return; // Everything is fine. Escape as soon as possible
} else {
try {
// This was reported as working on some devices, which I really hope is just the race condition
// kicking in, otherwise something is seriously wrong with the permission system on those devices.
// We will try it anyway, since starting a loop will be slower by many magnitudes.
filesDir.mkdirs();
} catch (SecurityException ignored) {
}
}
}
if (filesDir == null || !filesDir.exists()) {
// Wait a "reasonable" amount of time before quitting.
// In this case we define reasonable as 200 ms (~12 dropped frames) before giving up (which most likely
// will result in the app crashing). This lag would only be seen in worst case scenarios, and then, only
// when the app is started the first time.
long[] timeoutsMs = new long[]{1, 2, 5, 10, 16}; // Exponential waits, capped at 16 ms;
long maxTotalWaitMs = 200;
long currentTotalWaitMs = 0;
int waitIndex = -1;
while (context.getFilesDir() == null || !context.getFilesDir().exists()) {
long waitMs = timeoutsMs[Math.min(++waitIndex, timeoutsMs.length - 1)];
SystemClock.sleep(waitMs);
currentTotalWaitMs += waitMs;
if (currentTotalWaitMs > maxTotalWaitMs) {
break;
}
}
}
// One final check before giving up
if (context.getFilesDir() == null || !context.getFilesDir().exists()) {
throw new IllegalStateException("Context.getFilesDir() returns " + context.getFilesDir() + " which is not an existing directory. See https://issuetracker.google.com/issues/36918154");
}
}
/**
* Realm static constructor that returns the Realm instance defined by the {@link io.realm.RealmConfiguration} set
* by {@link #setDefaultConfiguration(RealmConfiguration)}
*
* @return an instance of the Realm class.
* @throws java.lang.NullPointerException if no default configuration has been defined.
* @throws RealmMigrationNeededException if no migration has been provided by the default configuration and the
* RealmObject classes or version has has changed so a migration is required.
* @throws RealmFileException if an error happened when accessing the underlying Realm file.
* @throws io.realm.exceptions.DownloadingRealmInterruptedException if {@link SyncConfiguration.Builder#waitForInitialRemoteData()}
* was set and the thread opening the Realm was interrupted while the download was in progress.
*/
public static Realm getDefaultInstance() {
if (defaultConfiguration == null) {
throw new IllegalStateException("Call `Realm.init(Context)` before calling this method.");
}
return RealmCache.createRealmOrGetFromCache(defaultConfiguration, Realm.class);
}
/**
* Realm static constructor that returns the Realm instance defined by provided {@link io.realm.RealmConfiguration}
*
* @param configuration {@link RealmConfiguration} used to open the Realm
* @return an instance of the Realm class
* @throws RealmMigrationNeededException if no migration has been provided by the configuration and the RealmObject
* classes or version has has changed so a migration is required.
* @throws RealmFileException if an error happened when accessing the underlying Realm file.
* @throws IllegalArgumentException if a null {@link RealmConfiguration} is provided.
* @throws io.realm.exceptions.DownloadingRealmInterruptedException if {@link SyncConfiguration.Builder#waitForInitialRemoteData()}
* was set and the thread opening the Realm was interrupted while the download was in progress.
* @see RealmConfiguration for details on how to configure a Realm.
*/
public static Realm getInstance(RealmConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(NULL_CONFIG_MSG);
}
return RealmCache.createRealmOrGetFromCache(configuration, Realm.class);
}
/**
* The creation of the first Realm instance per {@link RealmConfiguration} in a process can take some time as all
* initialization code need to run at that point (setting up the Realm, validating schemas and creating initial
* data). This method places the initialization work in a background thread and deliver the Realm instance
* to the caller thread asynchronously after the initialization is finished.
*
* @param configuration {@link RealmConfiguration} used to open the Realm.
* @param callback invoked to return the results.
* @throws IllegalArgumentException if a null {@link RealmConfiguration} or a null {@link Callback} is provided.
* @throws IllegalStateException if it is called from a non-Looper or {@link IntentService} thread.
* @return a {@link RealmAsyncTask} representing a cancellable task.
* @see Callback for more details.
*/
public static RealmAsyncTask getInstanceAsync(RealmConfiguration configuration,
Callback callback) {
if (configuration == null) {
throw new IllegalArgumentException(NULL_CONFIG_MSG);
}
return RealmCache.createRealmOrGetFromCacheAsync(configuration, callback, Realm.class);
}
/**
* Sets the {@link io.realm.RealmConfiguration} used when calling {@link #getDefaultInstance()}.
*
* @param configuration the {@link io.realm.RealmConfiguration} to use as the default configuration.
* @throws IllegalArgumentException if a null {@link RealmConfiguration} is provided.
* @see RealmConfiguration for details on how to configure a Realm.
*/
public static void setDefaultConfiguration(RealmConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("A non-null RealmConfiguration must be provided");
}
defaultConfiguration = configuration;
}
/**
* Removes the current default configuration (if any). Any further calls to {@link #getDefaultInstance()} will
* fail until a new default configuration has been set using {@link #setDefaultConfiguration(RealmConfiguration)}.
*/
public static void removeDefaultConfiguration() {
defaultConfiguration = null;
}
/**
* Creates a {@link Realm} instance without checking the existence in the {@link RealmCache}.
*
* @param cache the {@link RealmCache} where to create the realm in.
* @return a {@link Realm} instance.
*/
static Realm createInstance(RealmCache cache) {
RealmConfiguration configuration = cache.getConfiguration();
try {
return createAndValidateFromCache(cache);
} catch (RealmMigrationNeededException e) {
if (configuration.shouldDeleteRealmIfMigrationNeeded()) {
deleteRealm(configuration);
} else {
try {
if (configuration.getMigration() != null) {
migrateRealm(configuration, e);
}
} catch (FileNotFoundException fileNotFoundException) {
// Should never happen.
throw new RealmFileException(RealmFileException.Kind.NOT_FOUND, fileNotFoundException);
}
}
return createAndValidateFromCache(cache);
}
}
private static Realm createAndValidateFromCache(RealmCache cache) {
Realm realm = new Realm(cache);
RealmConfiguration configuration = realm.configuration;
final long currentVersion = realm.getVersion();
final long requiredVersion = configuration.getSchemaVersion();
final ColumnIndices columnIndices = RealmCache.findColumnIndices(cache.getTypedColumnIndicesArray(),
requiredVersion);
if (columnIndices != null) {
// Copies global cache as a Realm local indices cache.
realm.schema.setInitialColumnIndices(columnIndices);
} else {
final boolean syncingConfig = configuration.isSyncConfiguration();
if (!syncingConfig && (currentVersion != UNVERSIONED)) {
if (currentVersion < requiredVersion) {
realm.doClose();
throw new RealmMigrationNeededException(
configuration.getPath(),
String.format("Realm on disk need to migrate from v%s to v%s", currentVersion, requiredVersion));
}
if (requiredVersion < currentVersion) {
realm.doClose();
throw new IllegalArgumentException(
String.format("Realm on disk is newer than the one specified: v%s vs. v%s", currentVersion, requiredVersion));
}
}
// Initializes Realm schema if needed.
try {
if (!syncingConfig) {
initializeRealm(realm);
} else {
initializeSyncedRealm(realm);
}
} catch (RuntimeException e) {
realm.doClose();
throw e;
}
}
return realm;
}
private static void initializeRealm(Realm realm) {
// Everything in this method needs to be behind a transaction lock to prevent multi-process interaction while
// the Realm is initialized.
boolean commitChanges = false;
try {
// We need to start a transaction no matter readOnly mode, because it acts as an interprocess lock.
// TODO: For proper inter-process support we also need to move e.g copying the asset file under an
// interprocess lock. This lock can obviously not be created by a Realm instance so we probably need
// to implement it in Object Store. When this happens, the `beginTransaction(true)` can be removed again.
realm.beginTransaction(true);
long currentVersion = realm.getVersion();
boolean unversioned = currentVersion == UNVERSIONED;
commitChanges = unversioned;
RealmConfiguration configuration = realm.getConfiguration();
RealmProxyMediator mediator = configuration.getSchemaMediator();
Set<Class<? extends RealmModel>> modelClasses = mediator.getModelClasses();
// Only allow creating the schema if not in read-only mode
if (unversioned) {
if (configuration.isReadOnly()) {
throw new IllegalArgumentException("Cannot create the Realm schema in a read-only file.");
}
realm.setVersion(configuration.getSchemaVersion());
// Create all of the tables.
for (Class<? extends RealmModel> modelClass : modelClasses) {
mediator.createRealmObjectSchema(modelClass, realm.getSchema());
}
}
// Now that they have all been created, validate them.
final Map<Class<? extends RealmModel>, ColumnInfo> columnInfoMap = new HashMap<>(modelClasses.size());
for (Class<? extends RealmModel> modelClass : modelClasses) {
columnInfoMap.put(modelClass, mediator.validateTable(modelClass, realm.sharedRealm, false));
}
realm.getSchema().setInitialColumnIndices(
(unversioned) ? configuration.getSchemaVersion() : currentVersion,
columnInfoMap);
// Finally add any initial data
final Transaction transaction = configuration.getInitialDataTransaction();
if (transaction != null && unversioned) {
transaction.execute(realm);
}
} catch (Exception e) {
commitChanges = false;
throw e;
} finally {
if (commitChanges) {
realm.commitTransaction();
} else if (realm.isInTransaction()) {
realm.cancelTransaction();
}
}
}
// Everything in this method needs to be behind a transaction lock
// to prevent multi-process interaction while the Realm is initialized.
private static void initializeSyncedRealm(Realm realm) {
boolean commitChanges = false;
OsRealmSchema schema = null;
OsRealmSchema.Creator schemaCreator = null;
try {
// We need to start a transaction no matter readOnly mode, because it acts as an interprocess lock.
// TODO: For proper inter-process support we also need to move e.g copying the asset file under an
// interprocess lock. This lock can obviously not be created by a Realm instance so we probably need
// to implement it in Object Store. When this happens, the `beginTransaction(true)` can be removed again.
realm.beginTransaction(true);
long currentVersion = realm.getVersion();
final boolean unversioned = currentVersion == UNVERSIONED;
RealmConfiguration configuration = realm.getConfiguration();
final RealmProxyMediator mediator = configuration.getSchemaMediator();
final Set<Class<? extends RealmModel>> modelClasses = mediator.getModelClasses();
long newVersion = configuration.getSchemaVersion();
// Update/create the schema if allowed
if (!configuration.isReadOnly()) {
schemaCreator = new OsRealmSchema.Creator();
for (Class<? extends RealmModel> modelClass : modelClasses) {
mediator.createRealmObjectSchema(modelClass, schemaCreator);
}
// Assumption: When SyncConfiguration then additive schema update mode.
schema = new OsRealmSchema(schemaCreator);
schemaCreator.close();
schemaCreator = null;
// !!! FIXME: This appalling kludge is necessitated by current package structure/visiblity constraints.
// It absolutely breaks encapsulation and needs to be fixed!
if (realm.sharedRealm.requiresMigration(schema.getNativePtr())) {
if (currentVersion >= newVersion) {
throw new IllegalArgumentException(String.format(
"The schema was changed but the schema version was not updated. " +
"The configured schema version (%d) must be greater than the version " +
" in the Realm file (%d) in order to update the schema.",
newVersion, currentVersion));
}
realm.sharedRealm.updateSchema(schema.getNativePtr(), newVersion);
// The OS currently does not handle setting the schema version. We have to do it manually.
realm.setVersion(newVersion);
commitChanges = true;
}
}
// Validate the schema in the file
final Map<Class<? extends RealmModel>, ColumnInfo> columnInfoMap = new HashMap<>(modelClasses.size());
for (Class<? extends RealmModel> modelClass : modelClasses) {
columnInfoMap.put(modelClass, mediator.validateTable(modelClass, realm.sharedRealm, false));
}
realm.getSchema().setInitialColumnIndices((unversioned) ? newVersion : currentVersion, columnInfoMap);
if (unversioned && !configuration.isReadOnly()) {
final Transaction transaction = configuration.getInitialDataTransaction();
if (transaction != null) {
transaction.execute(realm);
}
}
} catch (RuntimeException e) {
commitChanges = false;
throw e;
} finally {
if (schemaCreator != null) {
schemaCreator.close();
}
if (schema != null) {
schema.close();
}
if (commitChanges) {
realm.commitTransaction();
} else {
realm.cancelTransaction();
}
}
}
/**
* Creates a Realm object for each object in a JSON array. This must be done within a transaction.
* <p>
* JSON properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the
* JSON object the {@link RealmObject} field will be set to the default value for that type.
*
* @param clazz type of Realm objects to create.
* @param json an array where each JSONObject must map to the specified class.
* @throws RealmException if mapping from JSON fails.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
*/
public <E extends RealmModel> void createAllFromJson(Class<E> clazz, JSONArray json) {
if (clazz == null || json == null) {
return;
}
checkIfValid();
for (int i = 0; i < json.length(); i++) {
try {
configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json.getJSONObject(i), false);
} catch (JSONException e) {
throw new RealmException("Could not map JSON", e);
}
}
}
/**
* Tries to update a list of existing objects identified by their primary key with new JSON data. If an existing
* object could not be found in the Realm, a new object will be created. This must happen within a transaction.
* If updating a {@link RealmObject} and a field is not found in the JSON object, that field will not be updated. If
* a new {@link RealmObject} is created and a field is not found in the JSON object, that field will be assigned the
* default value for the field type.
*
* @param clazz type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param json array with object data.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws RealmException if unable to map JSON.
* @see #createAllFromJson(Class, org.json.JSONArray)
*/
public <E extends RealmModel> void createOrUpdateAllFromJson(Class<E> clazz, JSONArray json) {
if (clazz == null || json == null) {
return;
}
checkIfValid();
checkHasPrimaryKey(clazz);
for (int i = 0; i < json.length(); i++) {
try {
configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json.getJSONObject(i), true);
} catch (JSONException e) {
throw new RealmException("Could not map JSON", e);
}
}
}
/**
* Creates a Realm object for each object in a JSON array. This must be done within a transaction.
* JSON properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the
* JSON object the {@link RealmObject} field will be set to the default value for that type.
*
* @param clazz type of Realm objects to create.
* @param json the JSON array as a String where each object can map to the specified class.
* @throws RealmException if mapping from JSON fails.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
*/
public <E extends RealmModel> void createAllFromJson(Class<E> clazz, String json) {
if (clazz == null || json == null || json.length() == 0) {
return;
}
JSONArray arr;
try {
arr = new JSONArray(json);
} catch (JSONException e) {
throw new RealmException("Could not create JSON array from string", e);
}
createAllFromJson(clazz, arr);
}
/**
* Tries to update a list of existing objects identified by their primary key with new JSON data. If an existing
* object could not be found in the Realm, a new object will be created. This must happen within a transaction.
* If updating a {@link RealmObject} and a field is not found in the JSON object, that field will not be updated.
* If a new {@link RealmObject} is created and a field is not found in the JSON object, that field will be assigned
* the default value for the field type.
*
* @param clazz type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param json string with an array of JSON objects.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws RealmException if unable to create a JSON array from the json string.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @see #createAllFromJson(Class, String)
*/
public <E extends RealmModel> void createOrUpdateAllFromJson(Class<E> clazz, String json) {
if (clazz == null || json == null || json.length() == 0) {
return;
}
checkIfValid();
checkHasPrimaryKey(clazz);
JSONArray arr;
try {
arr = new JSONArray(json);
} catch (JSONException e) {
throw new RealmException("Could not create JSON array from string", e);
}
createOrUpdateAllFromJson(clazz, arr);
}
/**
* Creates a Realm object for each object in a JSON array. This must be done within a transaction.
* JSON properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the
* JSON object the {@link RealmObject} field will be set to the default value for that type.
* <p>
* This API is only available in API level 11 or later.
*
* @param clazz type of Realm objects created.
* @param inputStream the JSON array as a InputStream. All objects in the array must be of the specified class.
* @throws RealmException if mapping from JSON fails.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws IOException if something was wrong with the input stream.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public <E extends RealmModel> void createAllFromJson(Class<E> clazz, InputStream inputStream) throws IOException {
if (clazz == null || inputStream == null) {
return;
}
checkIfValid();
JsonReader reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8"));
try {
reader.beginArray();
while (reader.hasNext()) {
configuration.getSchemaMediator().createUsingJsonStream(clazz, this, reader);
}
reader.endArray();
} finally {
reader.close();
}
}
/**
* Tries to update a list of existing objects identified by their primary key with new JSON data. If an existing
* object could not be found in the Realm, a new object will be created. This must happen within a transaction.
* If updating a {@link RealmObject} and a field is not found in the JSON object, that field will not be updated.
* If a new {@link RealmObject} is created and a field is not found in the JSON object, that field will be assigned
* the default value for the field type.
* <p>
* This API is only available in API level 11 or later.
*
* @param clazz type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param in the InputStream with a list of object data in JSON format.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws RealmException if unable to read JSON.
* @see #createOrUpdateAllFromJson(Class, java.io.InputStream)
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public <E extends RealmModel> void createOrUpdateAllFromJson(Class<E> clazz, InputStream in) {
if (clazz == null || in == null) {
return;
}
checkIfValid();
checkHasPrimaryKey(clazz);
// As we need the primary key value we have to first parse the entire input stream as in the general
// case that value might be the last property. :(
Scanner scanner = null;
try {
scanner = getFullStringScanner(in);
JSONArray json = new JSONArray(scanner.next());
for (int i = 0; i < json.length(); i++) {
configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json.getJSONObject(i), true);
}
} catch (JSONException e) {
throw new RealmException("Failed to read JSON", e);
} finally {
if (scanner != null) {
scanner.close();
}
}
}
/**
* Creates a Realm object pre-filled with data from a JSON object. This must be done inside a transaction. JSON
* properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the JSON
* object the {@link RealmObject} field will be set to the default value for that type.
*
* @param clazz type of Realm object to create.
* @param json the JSONObject with object data.
* @return created object or {@code null} if no JSON data was provided.
* @throws RealmException if the mapping from JSON fails.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @see #createOrUpdateObjectFromJson(Class, org.json.JSONObject)
*/
public <E extends RealmModel> E createObjectFromJson(Class<E> clazz, JSONObject json) {
if (clazz == null || json == null) {
return null;
}
checkIfValid();
try {
return configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json, false);
} catch (JSONException e) {
throw new RealmException("Could not map JSON", e);
}
}
/**
* Tries to update an existing object defined by its primary key with new JSON data. If no existing object could be
* found a new object will be saved in the Realm. This must happen within a transaction. If updating a {@link RealmObject}
* and a field is not found in the JSON object, that field will not be updated. If a new {@link RealmObject} is
* created and a field is not found in the JSON object, that field will be assigned the default value for the field type.
*
* @param clazz Type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param json {@link org.json.JSONObject} with object data.
* @return created or updated {@link io.realm.RealmObject}.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws RealmException if JSON data cannot be mapped.
* @see #createObjectFromJson(Class, org.json.JSONObject)
*/
public <E extends RealmModel> E createOrUpdateObjectFromJson(Class<E> clazz, JSONObject json) {
if (clazz == null || json == null) {
return null;
}
checkIfValid();
checkHasPrimaryKey(clazz);
try {
return configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json, true);
} catch (JSONException e) {
throw new RealmException("Could not map JSON", e);
}
}
/**
* Creates a Realm object pre-filled with data from a JSON object. This must be done inside a transaction. JSON
* properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the JSON
* object the {@link RealmObject} field will be set to the default value for that type.
*
* @param clazz type of Realm object to create.
* @param json the JSON string with object data.
* @return created object or {@code null} if JSON string was empty or null.
* @throws RealmException if mapping to json failed.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
*/
public <E extends RealmModel> E createObjectFromJson(Class<E> clazz, String json) {
if (clazz == null || json == null || json.length() == 0) {
return null;
}
JSONObject obj;
try {
obj = new JSONObject(json);
} catch (JSONException e) {
throw new RealmException("Could not create Json object from string", e);
}
return createObjectFromJson(clazz, obj);
}
/**
* Tries to update an existing object defined by its primary key with new JSON data. If no existing object could be
* found a new object will be saved in the Realm. This must happen within a transaction. If updating a
* {@link RealmObject} and a field is not found in the JSON object, that field will not be updated. If a new
* {@link RealmObject} is created and a field is not found in the JSON object, that field will be assigned the
* default value for the field type.
*
* @param clazz type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param json string with object data in JSON format.
* @return created or updated {@link io.realm.RealmObject}.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws RealmException if JSON object cannot be mapped from the string parameter.
* @see #createObjectFromJson(Class, String)
*/
public <E extends RealmModel> E createOrUpdateObjectFromJson(Class<E> clazz, String json) {
if (clazz == null || json == null || json.length() == 0) {
return null;
}
checkIfValid();
checkHasPrimaryKey(clazz);
JSONObject obj;
try {
obj = new JSONObject(json);
} catch (JSONException e) {
throw new RealmException("Could not create Json object from string", e);
}
return createOrUpdateObjectFromJson(clazz, obj);
}
/**
* Creates a Realm object pre-filled with data from a JSON object. This must be done inside a transaction. JSON
* properties with unknown properties will be ignored. If a {@link RealmObject} field is not present in the JSON
* object the {@link RealmObject} field will be set to the default value for that type.
* <p>
* This API is only available in API level 11 or later.
*
* @param clazz type of Realm object to create.
* @param inputStream the JSON object data as a InputStream.
* @return created object or {@code null} if JSON string was empty or null.
* @throws RealmException if the mapping from JSON failed.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws IOException if something went wrong with the input stream.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public <E extends RealmModel> E createObjectFromJson(Class<E> clazz, InputStream inputStream) throws IOException {
if (clazz == null || inputStream == null) {
return null;
}
checkIfValid();
E realmObject;
Table table = schema.getTable(clazz);
if (table.hasPrimaryKey()) {
// As we need the primary key value we have to first parse the entire input stream as in the general
// case that value might be the last property. :(
Scanner scanner = null;
try {
scanner = getFullStringScanner(inputStream);
JSONObject json = new JSONObject(scanner.next());
realmObject = configuration.getSchemaMediator().createOrUpdateUsingJsonObject(clazz, this, json, false);
} catch (JSONException e) {
throw new RealmException("Failed to read JSON", e);
} finally {
if (scanner != null) {
scanner.close();
}
}
} else {
JsonReader reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8"));
try {
realmObject = configuration.getSchemaMediator().createUsingJsonStream(clazz, this, reader);
} finally {
reader.close();
}
}
return realmObject;
}
/**
* Tries to update an existing object defined by its primary key with new JSON data. If no existing object could be
* found a new object will be saved in the Realm. This must happen within a transaction. If updating a
* {@link RealmObject} and a field is not found in the JSON object, that field will not be updated. If a new
* {@link RealmObject} is created and a field is not found in the JSON object, that field will be assigned the
* default value for the field type.
* <p>
* This API is only available in API level 11 or later.
*
* @param clazz type of {@link io.realm.RealmObject} to create or update. It must have a primary key defined.
* @param in the {@link InputStream} with object data in JSON format.
* @return created or updated {@link io.realm.RealmObject}.
* @throws IllegalArgumentException if trying to update a class without a {@link io.realm.annotations.PrimaryKey}.
* @throws IllegalArgumentException if the JSON object doesn't have a primary key property but the corresponding
* {@link RealmObjectSchema} has a {@link io.realm.annotations.PrimaryKey} defined.
* @throws RealmException if failure to read JSON.
* @see #createObjectFromJson(Class, java.io.InputStream)
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public <E extends RealmModel> E createOrUpdateObjectFromJson(Class<E> clazz, InputStream in) {
if (clazz == null || in == null) {
return null;
}
checkIfValid();
checkHasPrimaryKey(clazz);
// As we need the primary key value we have to first parse the entire input stream as in the general
// case that value might be the last property. :(
Scanner scanner = null;
try {
scanner = getFullStringScanner(in);
JSONObject json = new JSONObject(scanner.next());
return createOrUpdateObjectFromJson(clazz, json);
} catch (JSONException e) {
throw new RealmException("Failed to read JSON", e);
} finally {
if (scanner != null) {
scanner.close();
}
}
}
private Scanner getFullStringScanner(InputStream in) {
return new Scanner(in, "UTF-8").useDelimiter("\\A");
}
/**
* Instantiates and adds a new object to the Realm.
* <p>
* This method is only available for model classes with no @PrimaryKey annotation.
* If you like to create an object that has a primary key, use {@link #createObject(Class, Object)}
* or {@link #copyToRealm(RealmModel)} instead.
*
* @param clazz the Class of the object to create.
* @return the new object.
* @throws RealmException if the primary key is defined in the model class or an object cannot be created.
* @see #createObject(Class, Object)
*/
public <E extends RealmModel> E createObject(Class<E> clazz) {
checkIfValid();
return createObjectInternal(clazz, true, Collections.<String>emptyList());
}
/**
* Same as {@link #createObject(Class)} but this does not check the thread.
*
* @param clazz the Class of the object to create.
* @param acceptDefaultValue if {@code true}, default value of the object will be applied and
* if {@code false}, it will be ignored.
* @return the new object.
* @throws RealmException if the primary key is defined in the model class or an object cannot be created.
*/
// Called from proxy classes.
<E extends RealmModel> E createObjectInternal(
Class<E> clazz,
boolean acceptDefaultValue,
List<String> excludeFields) {
Table table = schema.getTable(clazz);
// Checks and throws the exception earlier for a better exception message.
if (table.hasPrimaryKey()) {
throw new RealmException(String.format("'%s' has a primary key, use" +
" 'createObject(Class<E>, Object)' instead.", table.getClassName()));
}
return configuration.getSchemaMediator().newInstance(clazz, this,
OsObject.create(sharedRealm, table),
schema.getColumnInfo(clazz),
acceptDefaultValue, excludeFields);
}
/**
* Instantiates and adds a new object to the Realm with the primary key value already set.
* <p>
* If the value violates the primary key constraint, no object will be added and a {@link RealmException} will be
* thrown.
* The default value for primary key provided by the model class will be ignored.
*
* @param clazz the Class of the object to create.
* @param primaryKeyValue value for the primary key field.
* @return the new object.
* @throws RealmException if object could not be created due to the primary key being invalid.
* @throws IllegalStateException if the model class does not have an primary key defined.
* @throws IllegalArgumentException if the {@code primaryKeyValue} doesn't have a value that can be converted to the
* expected value.
*/
public <E extends RealmModel> E createObject(Class<E> clazz, Object primaryKeyValue) {
checkIfValid();
return createObjectInternal(clazz, primaryKeyValue, true, Collections.<String>emptyList());
}
/**
* Same as {@link #createObject(Class, Object)} but this does not check the thread.
*
* @param clazz the Class of the object to create.
* @param primaryKeyValue value for the primary key field.
* @param acceptDefaultValue if {@code true}, default value of the object will be applied and
* if {@code false}, it will be ignored.
* @return the new object.
* @throws RealmException if object could not be created due to the primary key being invalid.
* @throws IllegalStateException if the model class does not have an primary key defined.
* @throws IllegalArgumentException if the {@code primaryKeyValue} doesn't have a value that can be converted to the
* expected value.
*/
// Called from proxy classes.
<E extends RealmModel> E createObjectInternal(
Class<E> clazz,
Object primaryKeyValue,
boolean acceptDefaultValue,
List<String> excludeFields) {
Table table = schema.getTable(clazz);
return configuration.getSchemaMediator().newInstance(clazz, this,
OsObject.createWithPrimaryKey(sharedRealm, table, primaryKeyValue),
schema.getColumnInfo(clazz),
acceptDefaultValue, excludeFields);
}
/**
* Copies a RealmObject to the Realm instance and returns the copy. Any further changes to the original RealmObject
* will not be reflected in the Realm copy. This is a deep copy, so all referenced objects will be copied. Objects
* already in this Realm will be ignored.
* <p>
* Please note, copying an object will copy all field values. Any unset field in this and child objects will be
* set to their default value if not provided.
*
* @param object the {@link io.realm.RealmObject} to copy to the Realm.
* @return a managed RealmObject with its properties backed by the Realm.
* @throws java.lang.IllegalArgumentException if the object is {@code null} or it belongs to a Realm instance
* in a different thread.
*/
public <E extends RealmModel> E copyToRealm(E object) {
checkNotNullObject(object);
return copyOrUpdate(object, false, new HashMap<RealmModel, RealmObjectProxy>());
}
/**
* Updates an existing RealmObject that is identified by the same {@link io.realm.annotations.PrimaryKey} or creates
* a new copy if no existing object could be found. This is a deep copy or update i.e., all referenced objects will be
* either copied or updated.
* <p>
* Please note, copying an object will copy all field values. Any unset field in the object and child objects will be
* set to their default value if not provided.
*
* @param object {@link io.realm.RealmObject} to copy or update.
* @return the new or updated RealmObject with all its properties backed by the Realm.
* @throws java.lang.IllegalArgumentException if the object is {@code null} or doesn't have a Primary key defined
* or it belongs to a Realm instance in a different thread.
* @see #copyToRealm(RealmModel)
*/
public <E extends RealmModel> E copyToRealmOrUpdate(E object) {
checkNotNullObject(object);
checkHasPrimaryKey(object.getClass());
return copyOrUpdate(object, true, new HashMap<RealmModel, RealmObjectProxy>());
}
/**
* Copies a collection of RealmObjects to the Realm instance and returns their copy. Any further changes to the
* original RealmObjects will not be reflected in the Realm copies. This is a deep copy i.e., all referenced objects
* will be copied. Objects already in this Realm will be ignored.
* <p>
* Please note, copying an object will copy all field values. Any unset field in the objects and child objects will be
* set to their default value if not provided.
*
* @param objects the RealmObjects to copy to the Realm.
* @return a list of the the converted RealmObjects that all has their properties managed by the Realm.
* @throws io.realm.exceptions.RealmException if any of the objects has already been added to Realm.
* @throws java.lang.IllegalArgumentException if any of the elements in the input collection is {@code null}.
*/
public <E extends RealmModel> List<E> copyToRealm(Iterable<E> objects) {
if (objects == null) {
return new ArrayList<>();
}
Map<RealmModel, RealmObjectProxy> cache = new HashMap<>();
ArrayList<E> realmObjects = new ArrayList<>();
for (E object : objects) {
checkNotNullObject(object);
realmObjects.add(copyOrUpdate(object, false, cache));
}
return realmObjects;
}
/**
* Inserts a list of an unmanaged RealmObjects. This is generally faster than {@link #copyToRealm(Iterable)} since it
* doesn't return the inserted elements, and performs minimum allocations and checks.
* After being inserted any changes to the original objects will not be persisted.
* <p>
* Please note:
* <ul>
* <li>
* We don't check if the provided objects are already managed or not, so inserting a managed object might duplicate it.
* Duplication will only happen if the object doesn't have a primary key. Objects with primary keys will never get duplicated.
* </li>
* <li>We don't create (nor return) a managed {@link RealmObject} for each element</li>
* <li>Copying an object will copy all field values. Any unset field in the object and child objects will be set to their default value if not provided</li>
* </ul>
* <p>
* If you want the managed {@link RealmObject} returned, use {@link #copyToRealm(Iterable)}, otherwise if
* you have a large number of object this method is generally faster.
*
* @param objects RealmObjects to insert.
* @throws IllegalStateException if the corresponding Realm is closed, called from an incorrect thread or not in a
* transaction.
* @see #copyToRealm(Iterable)
*/
public void insert(Collection<? extends RealmModel> objects) {
checkIfValidAndInTransaction();
if (objects == null) {
throw new IllegalArgumentException("Null objects cannot be inserted into Realm.");
}
if (objects.isEmpty()) {
return;
}
configuration.getSchemaMediator().insert(this, objects);
}
/**
* Inserts an unmanaged RealmObject. This is generally faster than {@link #copyToRealm(RealmModel)} since it
* doesn't return the inserted elements, and performs minimum allocations and checks.
* After being inserted any changes to the original object will not be persisted.
* <p>
* Please note:
* <ul>
* <li>
* We don't check if the provided objects are already managed or not, so inserting a managed object might duplicate it.
* Duplication will only happen if the object doesn't have a primary key. Objects with primary keys will never get duplicated.
* </li>
* <li>We don't create (nor return) a managed {@link RealmObject} for each element</li>
* <li>Copying an object will copy all field values. Any unset field in the object and child objects will be set to their default value if not provided</li>
* </ul>
* <p>
* If you want the managed {@link RealmObject} returned, use {@link #copyToRealm(RealmModel)}, otherwise if
* you have a large number of object this method is generally faster.
*
* @param object RealmObjects to insert.
* @throws IllegalStateException if the corresponding Realm is closed, called from an incorrect thread or not in a
* transaction.
* @throws io.realm.exceptions.RealmPrimaryKeyConstraintException if two objects with the same primary key is
* inserted or if a primary key value already exists in the Realm.
* @see #copyToRealm(RealmModel)
*/
public void insert(RealmModel object) {
checkIfValidAndInTransaction();
if (object == null) {
throw new IllegalArgumentException("Null object cannot be inserted into Realm.");
}
Map<RealmModel, Long> cache = new HashMap<>();
configuration.getSchemaMediator().insert(this, object, cache);
}
/**
* Inserts or updates a list of unmanaged RealmObjects. This is generally faster than
* {@link #copyToRealmOrUpdate(Iterable)} since it doesn't return the inserted elements, and performs minimum
* allocations and checks.
* After being inserted any changes to the original objects will not be persisted.
* <p>
* Please note:
* <ul>
* <li>
* We don't check if the provided objects are already managed or not, so inserting a managed object might duplicate it.
* Duplication will only happen if the object doesn't have a primary key. Objects with primary keys will never get duplicated.
* </li>
* <li>We don't create (nor return) a managed {@link RealmObject} for each element</li>
* <li>Copying an object will copy all field values. Any unset field in the object and child objects will be set to their default value if not provided</li>
* </ul>
* <p>
* If you want the managed {@link RealmObject} returned, use {@link #copyToRealm(Iterable)}, otherwise if
* you have a large number of object this method is generally faster.
*
* @param objects RealmObjects to insert.
* @throws IllegalStateException if the corresponding Realm is closed, called from an incorrect thread or not in a
* transaction.
* @throws io.realm.exceptions.RealmPrimaryKeyConstraintException if two objects with the same primary key is
* inserted or if a primary key value already exists in the Realm.
* @see #copyToRealmOrUpdate(Iterable)
*/
public void insertOrUpdate(Collection<? extends RealmModel> objects) {
checkIfValidAndInTransaction();
if (objects == null) {
throw new IllegalArgumentException("Null objects cannot be inserted into Realm.");
}
if (objects.isEmpty()) {
return;
}
configuration.getSchemaMediator().insertOrUpdate(this, objects);
}
/**
* Inserts or updates an unmanaged RealmObject. This is generally faster than
* {@link #copyToRealmOrUpdate(RealmModel)} since it doesn't return the inserted elements, and performs minimum
* allocations and checks.
* After being inserted any changes to the original object will not be persisted.
* <p>
* Please note:
* <ul>
* <li>
* We don't check if the provided objects are already managed or not, so inserting a managed object might duplicate it.
* Duplication will only happen if the object doesn't have a primary key. Objects with primary keys will never get duplicated.
* </li>
* <li>We don't create (nor return) a managed {@link RealmObject} for each element</li>
* <li>Copying an object will copy all field values. Any unset field in the object and child objects will be set to their default value if not provided</li>
* </ul>
* <p>
* If you want the managed {@link RealmObject} returned, use {@link #copyToRealm(RealmModel)}, otherwise if
* you have a large number of object this method is generally faster.
*
* @param object RealmObjects to insert.
* @throws IllegalStateException if the corresponding Realm is closed, called from an incorrect thread or not in a
* transaction.
* @see #copyToRealmOrUpdate(RealmModel)
*/
public void insertOrUpdate(RealmModel object) {
checkIfValidAndInTransaction();
if (object == null) {
throw new IllegalArgumentException("Null object cannot be inserted into Realm.");
}
Map<RealmModel, Long> cache = new HashMap<>();
configuration.getSchemaMediator().insertOrUpdate(this, object, cache);
}
/**
* Updates a list of existing RealmObjects that is identified by their {@link io.realm.annotations.PrimaryKey} or
* creates a new copy if no existing object could be found. This is a deep copy or update i.e., all referenced
* objects will be either copied or updated.
* <p>
* Please note, copying an object will copy all field values. Any unset field in the objects and child objects will be
* set to their default value if not provided.
*
* @param objects a list of objects to update or copy into Realm.
* @return a list of all the new or updated RealmObjects.
* @throws java.lang.IllegalArgumentException if RealmObject is {@code null} or doesn't have a Primary key defined.
* @see #copyToRealm(Iterable)
*/
public <E extends RealmModel> List<E> copyToRealmOrUpdate(Iterable<E> objects) {
if (objects == null) {
return new ArrayList<>(0);
}
Map<RealmModel, RealmObjectProxy> cache = new HashMap<>();
ArrayList<E> realmObjects = new ArrayList<>();
for (E object : objects) {
checkNotNullObject(object);
realmObjects.add(copyOrUpdate(object, true, cache));
}
return realmObjects;
}
/**
* Makes an unmanaged in-memory copy of already persisted RealmObjects. This is a deep copy that will copy all
* referenced objects.
* <p>
* The copied objects are all detached from Realm and they will no longer be automatically updated. This means
* that the copied objects might contain data that are no longer consistent with other managed Realm objects.
* <p>
* *WARNING*: Any changes to copied objects can be merged back into Realm using
* {@link #copyToRealmOrUpdate(RealmModel)}, but all fields will be overridden, not just those that were changed.
* This includes references to other objects, and can potentially override changes made by other threads.
*
* @param realmObjects RealmObjects to copy.
* @param <E> type of object.
* @return an in-memory detached copy of managed RealmObjects.
* @throws IllegalArgumentException if the RealmObject is no longer accessible or it is a {@link DynamicRealmObject}.
* @see #copyToRealmOrUpdate(Iterable)
*/
public <E extends RealmModel> List<E> copyFromRealm(Iterable<E> realmObjects) {
return copyFromRealm(realmObjects, Integer.MAX_VALUE);
}
/**
* Makes an unmanaged in-memory copy of already persisted RealmObjects. This is a deep copy that will copy all
* referenced objects up to the defined depth.
* <p>
* The copied objects are all detached from Realm and they will no longer be automatically updated. This means
* that the copied objects might contain data that are no longer consistent with other managed Realm objects.
* <p>
* *WARNING*: Any changes to copied objects can be merged back into Realm using
* {@link #copyToRealmOrUpdate(Iterable)}, but all fields will be overridden, not just those that were changed.
* This includes references to other objects even though they might be {@code null} due to {@code maxDepth} being
* reached. This can also potentially override changes made by other threads.
*
* @param realmObjects RealmObjects to copy.
* @param maxDepth limit of the deep copy. All references after this depth will be {@code null}. Starting depth is
* {@code 0}.
* @param <E> type of object.
* @return an in-memory detached copy of the RealmObjects.
* @throws IllegalArgumentException if {@code maxDepth < 0}, the RealmObject is no longer accessible or it is a
* {@link DynamicRealmObject}.
* @see #copyToRealmOrUpdate(Iterable)
*/
public <E extends RealmModel> List<E> copyFromRealm(Iterable<E> realmObjects, int maxDepth) {
checkMaxDepth(maxDepth);
if (realmObjects == null) {
return new ArrayList<>(0);
}
ArrayList<E> unmanagedObjects = new ArrayList<>();
Map<RealmModel, RealmObjectProxy.CacheData<RealmModel>> listCache = new HashMap<>();
for (E object : realmObjects) {
checkValidObjectForDetach(object);
unmanagedObjects.add(createDetachedCopy(object, maxDepth, listCache));
}
return unmanagedObjects;
}
/**
* Makes an unmanaged in-memory copy of an already persisted {@link RealmObject}. This is a deep copy that will copy
* all referenced objects.
* <p>
* The copied object(s) are all detached from Realm and they will no longer be automatically updated. This means
* that the copied objects might contain data that are no longer consistent with other managed Realm objects.
* <p>
* *WARNING*: Any changes to copied objects can be merged back into Realm using
* {@link #copyToRealmOrUpdate(RealmModel)}, but all fields will be overridden, not just those that were changed.
* This includes references to other objects, and can potentially override changes made by other threads.
*
* @param realmObject {@link RealmObject} to copy.
* @param <E> type of object.
* @return an in-memory detached copy of the managed {@link RealmObject}.
* @throws IllegalArgumentException if the RealmObject is no longer accessible or it is a {@link DynamicRealmObject}.
* @see #copyToRealmOrUpdate(RealmModel)
*/
public <E extends RealmModel> E copyFromRealm(E realmObject) {
return copyFromRealm(realmObject, Integer.MAX_VALUE);
}
/**
* Makes an unmanaged in-memory copy of an already persisted {@link RealmObject}. This is a deep copy that will copy
* all referenced objects up to the defined depth.
* <p>
* The copied object(s) are all detached from Realm and they will no longer be automatically updated. This means
* that the copied objects might contain data that are no longer consistent with other managed Realm objects.
* <p>
* *WARNING*: Any changes to copied objects can be merged back into Realm using
* {@link #copyToRealmOrUpdate(RealmModel)}, but all fields will be overridden, not just those that were changed.
* This includes references to other objects even though they might be {@code null} due to {@code maxDepth} being
* reached. This can also potentially override changes made by other threads.
*
* @param realmObject {@link RealmObject} to copy.
* @param maxDepth limit of the deep copy. All references after this depth will be {@code null}. Starting depth is
* {@code 0}.
* @param <E> type of object.
* @return an in-memory detached copy of the managed {@link RealmObject}.
* @throws IllegalArgumentException if {@code maxDepth < 0}, the RealmObject is no longer accessible or it is a
* {@link DynamicRealmObject}.
* @see #copyToRealmOrUpdate(RealmModel)
*/
public <E extends RealmModel> E copyFromRealm(E realmObject, int maxDepth) {
checkMaxDepth(maxDepth);
checkValidObjectForDetach(realmObject);
return createDetachedCopy(realmObject, maxDepth, new HashMap<RealmModel, RealmObjectProxy.CacheData<RealmModel>>());
}
/**
* Returns a typed RealmQuery, which can be used to query for specific objects of this type
*
* @param clazz the class of the object which is to be queried for.
* @return a typed RealmQuery, which can be used to query for specific objects of this type.
* @see io.realm.RealmQuery
*/
public <E extends RealmModel> RealmQuery<E> where(Class<E> clazz) {
checkIfValid();
return RealmQuery.createQuery(this, clazz);
}
/**
* Adds a change listener to the Realm.
* <p>
* The listeners will be executed when changes are committed by this or another thread.
* <p>
* Realm instances are per thread singletons and cached, so listeners should be
* removed manually even if calling {@link #close()}. Otherwise there is a
* risk of memory leaks.
*
* @param listener the change listener.
* @throws IllegalArgumentException if the change listener is {@code null}.
* @throws IllegalStateException if you try to register a listener from a non-Looper or {@link IntentService} thread.
* @see io.realm.RealmChangeListener
* @see #removeChangeListener(RealmChangeListener)
* @see #removeAllChangeListeners()
*/
public void addChangeListener(RealmChangeListener<Realm> listener) {
addListener(listener);
}
/**
* Removes the specified change listener.
*
* @param listener the change listener to be removed.
* @throws IllegalArgumentException if the change listener is {@code null}.
* @throws IllegalStateException if you try to remove a listener from a non-Looper Thread.
* @see io.realm.RealmChangeListener
*/
public void removeChangeListener(RealmChangeListener<Realm> listener) {
removeListener(listener);
}
/**
* Removes all user-defined change listeners.
*
* @throws IllegalStateException if you try to remove listeners from a non-Looper Thread.
* @see io.realm.RealmChangeListener
*/
public void removeAllChangeListeners() {
removeAllListeners();
}
/**
* Executes a given transaction on the Realm. {@link #beginTransaction()} and {@link #commitTransaction()} will be
* called automatically. If any exception is thrown during the transaction {@link #cancelTransaction()} will be
* called instead of {@link #commitTransaction()}.
*
* @param transaction the {@link io.realm.Realm.Transaction} to execute.
* @throws IllegalArgumentException if the {@code transaction} is {@code null}.
* @throws RealmMigrationNeededException if the latest version contains incompatible schema changes.
*/
public void executeTransaction(Transaction transaction) {
if (transaction == null) {
throw new IllegalArgumentException("Transaction should not be null");
}
beginTransaction();
try {
transaction.execute(this);
commitTransaction();
} catch (Throwable e) {
if (isInTransaction()) {
cancelTransaction();
} else {
RealmLog.warn("Could not cancel transaction, not currently in a transaction.");
}
throw e;
}
}
/**
* Similar to {@link #executeTransaction(Transaction)} but runs asynchronously on a worker thread.
*
* @param transaction {@link io.realm.Realm.Transaction} to execute.
* @return a {@link RealmAsyncTask} representing a cancellable task.
* @throws IllegalArgumentException if the {@code transaction} is {@code null}, or if the Realm is opened from
* another thread.
*/
public RealmAsyncTask executeTransactionAsync(final Transaction transaction) {
return executeTransactionAsync(transaction, null, null);
}
/**
* Similar to {@link #executeTransactionAsync(Transaction)}, but also accepts an OnSuccess callback.
*
* @param transaction {@link io.realm.Realm.Transaction} to execute.
* @param onSuccess callback invoked when the transaction succeeds.
* @return a {@link RealmAsyncTask} representing a cancellable task.
* @throws IllegalArgumentException if the {@code transaction} is {@code null}, or if the realm is opened from
* another thread.
*/
public RealmAsyncTask executeTransactionAsync(final Transaction transaction, final Realm.Transaction.OnSuccess onSuccess) {
if (onSuccess == null) {
throw new IllegalArgumentException("onSuccess callback can't be null");
}
return executeTransactionAsync(transaction, onSuccess, null);
}
/**
* Similar to {@link #executeTransactionAsync(Transaction)}, but also accepts an OnError callback.
*
* @param transaction {@link io.realm.Realm.Transaction} to execute.
* @param onError callback invoked when the transaction fails.
* @return a {@link RealmAsyncTask} representing a cancellable task.
* @throws IllegalArgumentException if the {@code transaction} is {@code null}, or if the realm is opened from
* another thread.
*/
public RealmAsyncTask executeTransactionAsync(final Transaction transaction, final Realm.Transaction.OnError onError) {
if (onError == null) {
throw new IllegalArgumentException("onError callback can't be null");
}
return executeTransactionAsync(transaction, null, onError);
}
/**
* Similar to {@link #executeTransactionAsync(Transaction)}, but also accepts an OnSuccess and OnError callbacks.
*
* @param transaction {@link io.realm.Realm.Transaction} to execute.
* @param onSuccess callback invoked when the transaction succeeds.
* @param onError callback invoked when the transaction fails.
* @return a {@link RealmAsyncTask} representing a cancellable task.
* @throws IllegalArgumentException if the {@code transaction} is {@code null}, or if the realm is opened from
* another thread.
*/
public RealmAsyncTask executeTransactionAsync(final Transaction transaction,
final Realm.Transaction.OnSuccess onSuccess,
final Realm.Transaction.OnError onError) {
checkIfValid();
if (transaction == null) {
throw new IllegalArgumentException("Transaction should not be null");
}
// Avoid to call canDeliverNotification() in bg thread.
final boolean canDeliverNotification = sharedRealm.capabilities.canDeliverNotification();
// If the user provided a Callback then we have to make sure the current Realm has an events looper to deliver
// the results.
if ((onSuccess != null || onError != null)) {
sharedRealm.capabilities.checkCanDeliverNotification("Callback cannot be delivered on current thread.");
}
// We need to use the same configuration to open a background SharedRealm (i.e Realm)
// to perform the transaction
final RealmConfiguration realmConfiguration = getConfiguration();
// We need to deliver the callback even if the Realm is closed. So acquire a reference to the notifier here.
final RealmNotifier realmNotifier = sharedRealm.realmNotifier;
final Future<?> pendingTransaction = asyncTaskExecutor.submitTransaction(new Runnable() {
@Override
public void run() {
if (Thread.currentThread().isInterrupted()) {
return;
}
SharedRealm.VersionID versionID = null;
Throwable exception = null;
final Realm bgRealm = Realm.getInstance(realmConfiguration);
bgRealm.beginTransaction();
try {
transaction.execute(bgRealm);
if (Thread.currentThread().isInterrupted()) {
return;
}
bgRealm.commitTransaction();
// The bgRealm needs to be closed before post event to caller's handler to avoid concurrency
// problem. This is currently guaranteed by posting callbacks later below.
versionID = bgRealm.sharedRealm.getVersionID();
} catch (final Throwable e) {
exception = e;
} finally {
try {
if (bgRealm.isInTransaction()) {
bgRealm.cancelTransaction();
}
} finally {
bgRealm.close();
}
}
final Throwable backgroundException = exception;
final SharedRealm.VersionID backgroundVersionID = versionID;
// Cannot be interrupted anymore.
if (canDeliverNotification) {
if (backgroundVersionID != null && onSuccess != null) {
realmNotifier.post(new Runnable() {
@Override
public void run() {
if (isClosed()) {
// The caller Realm is closed. Just call the onSuccess. Since the new created Realm
// cannot be behind the background one.
onSuccess.onSuccess();
return;
}
if (sharedRealm.getVersionID().compareTo(backgroundVersionID) < 0) {
sharedRealm.realmNotifier.addTransactionCallback(new Runnable() {
@Override
public void run() {
onSuccess.onSuccess();
}
});
} else {
onSuccess.onSuccess();
}
}
});
} else if (backgroundException != null) {
realmNotifier.post(new Runnable() {
@Override
public void run() {
if (onError != null) {
onError.onError(backgroundException);
} else {
throw new RealmException("Async transaction failed", backgroundException);
}
}
});
}
} else {
if (backgroundException != null) {
// FIXME: ThreadPoolExecutor will never throw the exception in the background.
// We need a redesign of the async transaction API.
// Throw in the worker thread since the caller thread cannot get notifications.
throw new RealmException("Async transaction failed", backgroundException);
}
}
}
});
return new RealmAsyncTaskImpl(pendingTransaction, asyncTaskExecutor);
}
/**
* Deletes all objects of the specified class from the Realm.
*
* @param clazz the class which objects should be removed.
* @throws IllegalStateException if the corresponding Realm is closed or called from an incorrect thread.
*/
public void delete(Class<? extends RealmModel> clazz) {
checkIfValid();
schema.getTable(clazz).clear();
}
@SuppressWarnings("unchecked")
private <E extends RealmModel> E copyOrUpdate(E object, boolean update, Map<RealmModel, RealmObjectProxy> cache) {
checkIfValid();
return configuration.getSchemaMediator().copyOrUpdate(this, object, update, cache);
}
private <E extends RealmModel> E createDetachedCopy(E object, int maxDepth, Map<RealmModel, RealmObjectProxy.CacheData<RealmModel>> cache) {
checkIfValid();
return configuration.getSchemaMediator().createDetachedCopy(object, maxDepth, cache);
}
private <E extends RealmModel> void checkNotNullObject(E object) {
if (object == null) {
throw new IllegalArgumentException("Null objects cannot be copied into Realm.");
}
}
private void checkHasPrimaryKey(Class<? extends RealmModel> clazz) {
if (!schema.getTable(clazz).hasPrimaryKey()) {
throw new IllegalArgumentException("A RealmObject with no @PrimaryKey cannot be updated: " + clazz.toString());
}
}
private void checkMaxDepth(int maxDepth) {
if (maxDepth < 0) {
throw new IllegalArgumentException("maxDepth must be > 0. It was: " + maxDepth);
}
}
private <E extends RealmModel> void checkValidObjectForDetach(E realmObject) {
if (realmObject == null) {
throw new IllegalArgumentException("Null objects cannot be copied from Realm.");
}
if (!(RealmObject.isManaged(realmObject) && RealmObject.isValid(realmObject))) {
throw new IllegalArgumentException("Only valid managed objects can be copied from Realm.");
}
if (realmObject instanceof DynamicRealmObject) {
throw new IllegalArgumentException("DynamicRealmObject cannot be copied from Realm.");
}
}
/**
* Manually triggers the migration associated with a given RealmConfiguration. If Realm is already at the latest
* version, nothing will happen.
*
* @param configuration {@link RealmConfiguration}
* @throws FileNotFoundException if the Realm file doesn't exist.
*/
public static void migrateRealm(RealmConfiguration configuration) throws FileNotFoundException {
migrateRealm(configuration, (RealmMigration) null);
}
/**
* Called when migration needed in the Realm initialization.
*
* @param configuration {@link RealmConfiguration}
* @param cause which triggers this migration.
* @throws FileNotFoundException if the Realm file doesn't exist.
*/
private static void migrateRealm(final RealmConfiguration configuration, final RealmMigrationNeededException cause)
throws FileNotFoundException {
BaseRealm.migrateRealm(configuration, null, new MigrationCallback() {
@Override
public void migrationComplete() {
}
}, cause);
}
/**
* Manually triggers a migration on a RealmMigration.
*
* @param configuration the{@link RealmConfiguration}.
* @param migration the {@link RealmMigration} to run on the Realm. This will override any migration set on the
* configuration.
* @throws FileNotFoundException if the Realm file doesn't exist.
*/
public static void migrateRealm(RealmConfiguration configuration, RealmMigration migration)
throws FileNotFoundException {
BaseRealm.migrateRealm(configuration, migration, new MigrationCallback() {
@Override
public void migrationComplete() {
}
}, null);
}
/**
* Deletes the Realm file specified by the given {@link RealmConfiguration} from the filesystem.
* All Realm instances must be closed before calling this method.
*
* @param configuration a {@link RealmConfiguration}.
* @return {@code false} if a file could not be deleted. The failing file will be logged.
* @throws IllegalStateException if not all realm instances are closed.
*/
public static boolean deleteRealm(RealmConfiguration configuration) {
return BaseRealm.deleteRealm(configuration);
}
/**
* Compacts a Realm file. A Realm file usually contain free/unused space.
* This method removes this free space and the file size is thereby reduced.
* Objects within the Realm files are untouched.
* <p>
* The file must be closed before this method is called, otherwise {@code false} will be returned.<br>
* The file system should have free space for at least a copy of the Realm file.<br>
* The Realm file is left untouched if any file operation fails.<br>
*
* @param configuration a {@link RealmConfiguration} pointing to a Realm file.
* @return {@code true} if successful, {@code false} if any file operation failed.
* @throws UnsupportedOperationException if Realm is synchronized.
*/
public static boolean compactRealm(RealmConfiguration configuration) {
// FIXME: remove this restriction when https://github.com/realm/realm-core/issues/2345 is resolved
if (configuration.isSyncConfiguration()) {
throw new UnsupportedOperationException("Compacting is not supported yet on synced Realms. See https://github.com/realm/realm-core/issues/2345");
}
return BaseRealm.compactRealm(configuration);
}
Table getTable(Class<? extends RealmModel> clazz) {
return schema.getTable(clazz);
}
/**
* Updates own schema cache.
*
* @param globalCacheArray global cache of column indices. If it contains an entry for current
* schema version, this method only copies the indices information in the entry.
* @return newly created indices information for current schema version. Or {@code null} if {@code globalCacheArray}
* already contains the entry for current schema version.
*/
ColumnIndices updateSchemaCache(ColumnIndices[] globalCacheArray) {
final long currentSchemaVersion = sharedRealm.getSchemaVersion();
final long cacheSchemaVersion = schema.getSchemaVersion();
if (currentSchemaVersion == cacheSchemaVersion) {
return null;
}
ColumnIndices createdGlobalCache = null;
ColumnIndices cacheForCurrentVersion = RealmCache.findColumnIndices(globalCacheArray,
currentSchemaVersion);
if (cacheForCurrentVersion == null) {
final RealmProxyMediator mediator = getConfiguration().getSchemaMediator();
// Not found in global cache. create it.
final Set<Class<? extends RealmModel>> modelClasses = mediator.getModelClasses();
final Map<Class<? extends RealmModel>, ColumnInfo> map;
map = new HashMap<>(modelClasses.size());
// This code may throw a RealmMigrationNeededException
//noinspection CaughtExceptionImmediatelyRethrown
try {
for (Class<? extends RealmModel> clazz : modelClasses) {
final ColumnInfo columnInfo = mediator.validateTable(clazz, sharedRealm, true);
map.put(clazz, columnInfo);
}
} catch (RealmMigrationNeededException e) {
throw e;
}
cacheForCurrentVersion = createdGlobalCache = new ColumnIndices(currentSchemaVersion, map);
}
schema.updateColumnIndices(cacheForCurrentVersion);
return createdGlobalCache;
}
/**
* Returns the default Realm module. This module contains all Realm classes in the current project, but not those
* from library or project dependencies. Realm classes in these should be exposed using their own module.
*
* @return the default Realm module or {@code null} if no default module exists.
* @throws RealmException if unable to create an instance of the module.
* @see io.realm.RealmConfiguration.Builder#modules(Object, Object...)
*/
public static Object getDefaultModule() {
String moduleName = "io.realm.DefaultRealmModule";
Class<?> clazz;
//noinspection TryWithIdenticalCatches
try {
clazz = Class.forName(moduleName);
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
return constructor.newInstance();
} catch (ClassNotFoundException e) {
return null;
} catch (InvocationTargetException e) {
throw new RealmException("Could not create an instance of " + moduleName, e);
} catch (InstantiationException e) {
throw new RealmException("Could not create an instance of " + moduleName, e);
} catch (IllegalAccessException e) {
throw new RealmException("Could not create an instance of " + moduleName, e);
}
}
/**
* Returns the current number of open Realm instances across all threads that are using this configuration.
* This includes both dynamic and normal Realms.
*
* @param configuration the {@link io.realm.RealmConfiguration} for the Realm.
* @return number of open Realm instances across all threads.
*/
public static int getGlobalInstanceCount(RealmConfiguration configuration) {
final AtomicInteger globalCount = new AtomicInteger(0);
RealmCache.invokeWithGlobalRefCount(configuration, new RealmCache.Callback() {
@Override
public void onResult(int count) {
globalCount.set(count);
}
});
return globalCount.get();
}
/**
* Returns the current number of open Realm instances on the thread calling this method. This include both
* dynamic and normal Realms.
*
* @param configuration the {@link io.realm.RealmConfiguration} for the Realm.
* @return number of open Realm instances across all threads.
*/
public static int getLocalInstanceCount(RealmConfiguration configuration) {
return RealmCache.getLocalThreadCount(configuration);
}
/**
* Encapsulates a Realm transaction.
* <p>
* Using this class will automatically handle {@link #beginTransaction()} and {@link #commitTransaction()}
* If any exception is thrown during the transaction {@link #cancelTransaction()} will be called instead of
* {@link #commitTransaction()}.
*/
public interface Transaction {
void execute(Realm realm);
/**
* Callback invoked to notify the caller thread.
*/
class Callback {
public void onSuccess() {}
public void onError(Exception ignore) {}
}
/**
* Callback invoked to notify the caller thread about the success of the transaction.
*/
interface OnSuccess {
void onSuccess();
}
/**
* Callback invoked to notify the caller thread about error during the transaction.
* The transaction will be rolled back and the background Realm will be closed before
* invoking {@link #onError(Throwable)}.
*/
interface OnError {
void onError(Throwable error);
}
}
/**
* {@inheritDoc}
*/
public static abstract class Callback extends InstanceCallback<Realm> {
/**
* {@inheritDoc}
*/
@Override
public abstract void onSuccess(Realm realm);
/**
* {@inheritDoc}
*/
@Override
public void onError(Throwable exception) {
super.onError(exception);
}
}
}